﻿namespace OpenComposite.Repository.Tracking
{
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.ComponentModel.Design.Serialization;
	using System.Data;
	using System.Data.SqlClient;
	using System.Data.SqlTypes;
	using System.Globalization;
	using System.IO;
	using System.Runtime.CompilerServices;
	using System.Runtime.Serialization.Formatters.Binary;
	using System.Workflow.ComponentModel;
	using System.Workflow.ComponentModel.Serialization;
	using System.Workflow.Runtime;
	using System.Workflow.Runtime.Tracking;
	using System.Xml;

	public class TrackingWorkflowInstance
	{
		#region Initialization

		internal TrackingWorkflowInstance(string connectionString)
		{
			_currDT = DateTime.UtcNow;
			_actMinDT = SqlDateTime.MinValue.Value;
			_userMinDT = SqlDateTime.MinValue.Value;
			_instMinDT = SqlDateTime.MinValue.Value;
			_childMinDT = SqlDateTime.MinValue.Value;
			_changesMinDT = SqlDateTime.MinValue.Value;
			_invMinDT = SqlDateTime.MinValue.Value;
			_internalId = -1L;
			_invoker = Guid.Empty;
			_activityEvents = null;
			_userEvents = new List<UserTrackingRecord>();
			_workflowEvents = new List<WorkflowTrackingRecord>();
			_invoked = new List<TrackingWorkflowInstance>();
			if ( connectionString == null ) {
				throw new ArgumentNullException("connectionString");
			}
			_connectionString = connectionString;
		}

		private TrackingWorkflowInstance()
		{
			_currDT = DateTime.UtcNow;
			_actMinDT = SqlDateTime.MinValue.Value;
			_userMinDT = SqlDateTime.MinValue.Value;
			_instMinDT = SqlDateTime.MinValue.Value;
			_childMinDT = SqlDateTime.MinValue.Value;
			_changesMinDT = SqlDateTime.MinValue.Value;
			_invMinDT = SqlDateTime.MinValue.Value;
			_internalId = -1L;
			_invoker = Guid.Empty;
			_activityEvents = new List<ActivityTrackingRecord>();
			_userEvents = new List<UserTrackingRecord>();
			_workflowEvents = new List<WorkflowTrackingRecord>();
			_invoked = new List<TrackingWorkflowInstance>();
		}

		#endregion Initialization

		#region Public Members

		#region Properties

		public IList<ActivityTrackingRecord> ActivityEvents
		{
			get
			{
				if ( _autoRefresh ) {
					Refresh();
				}
				if ( _activityEvents == null ) {
					loadActivityEvents();
				}
				return _activityEvents;
			}
		}

		public bool AutoRefresh
		{
			get { return _autoRefresh; }
			set { _autoRefresh = value; }
		}

		public DateTime Initialized
		{
			get;
			set;
		}

		public IList<TrackingWorkflowInstance> InvokedWorkflows
		{
			get
			{
				if ( _autoRefresh ) {
					Refresh();
				}
				loadInvokedWorkflows();
				return _invoked;
			}
		}

		public Guid InvokingWorkflowInstanceId
		{
			get { return _invoker; }
			set { _invoker = value; }
		}

		public WorkflowStatus Status
		{
			get;
			set;
		}

		public IList<UserTrackingRecord> UserEvents
		{
			get
			{
				if ( _autoRefresh ) {
					Refresh();
				}
				loadUserEvents();
				return _userEvents;
			}
		}

		public string WorkflowAssemblyName
		{
			get;
			set;
		}

		public Activity WorkflowDefinition
		{
			get
			{
				if ( _autoRefresh ) {
					Refresh();
				}
				loadDef();
				return _def;
			}
		}

		public bool WorkflowDefinitionUpdated
		{
			get
			{
				if ( _autoRefresh ) {
					Refresh();
				}
				loadDef();
				return false;
			}
		}

		public IList<WorkflowTrackingRecord> WorkflowEvents
		{
			get
			{
				if ( _autoRefresh ) {
					Refresh();
				}
				loadWorkflowEvents();
				return _workflowEvents;
			}
		}

		public string WorkflowFullTypeName
		{
			get;
			set;
		}

		public Guid WorkflowInstanceId
		{
			get { return _id; }
			set { _id = value; }
		}

		public long WorkflowInstanceInternalId
		{
			get { return _internalId; }
			set { _internalId = value; }
		}

		public Type WorkflowType
		{
			get;
			set;
		}

		public string WorkflowTypeName
		{
			get;
			set;
		}

		#endregion Properties

		#region Methods

		public void Refresh()
		{
			_currDT = DateTime.UtcNow;
		}

		public void LoadActivityEvents()
		{
			loadActivityEvents();
		}

		#endregion Methods

		#endregion Public Members

		#region Private Members

		#region Static Fields

		private static int _deadlock = 0x4b5;
		private static short _retries = 5;

		#endregion Static Fields

		#region Fields

		private DateTime _actMinDT;
		private List<ActivityTrackingRecord> _activityEvents;
		private bool _autoRefresh;
		private DateTime _changesMinDT;
		private DateTime _childMinDT;
		private string _connectionString;
		private DateTime _currDT;
		private Activity _def;
		private Guid _id;
		private DateTime _instMinDT;
		private long _internalId;
		private DateTime _invMinDT;
		private List<TrackingWorkflowInstance> _invoked;
		private Guid _invoker;
		private List<UserTrackingRecord> _userEvents;
		private DateTime _userMinDT;
		private List<WorkflowTrackingRecord> _workflowEvents;

		#endregion Fields

		#region Methods

		private void loadActivityEvents()
		{
			SqlCommand cmd = createInternalIdDateTimeCommand("[dbo].[GetActivityEventsWithDetails]", _actMinDT, _currDT);
			executeRetried(cmd, new LoadFromReader(loadActivityEventsFromReader));
		}

		private SqlCommand createInternalIdDateTimeCommand(string commandText, DateTime minDT, DateTime maxDT)
		{
			if ( commandText == null ) {
				throw new ArgumentNullException("commandText");
			}
			SqlCommand command = new SqlCommand(commandText);
			command.CommandType = CommandType.StoredProcedure;
			command.Parameters.Add(new SqlParameter("@WorkflowInstanceInternalId", _internalId));
			command.Parameters.Add(new SqlParameter("@BeginDateTime", minDT));
			command.Parameters.Add(new SqlParameter("@EndDateTime", maxDT));
			return command;
		}

		private void executeRetried(SqlCommand cmd, LoadFromReader loader)
		{
			executeRetried(cmd, loader, null);
		}

		private void executeRetried(SqlCommand cmd, LoadFromReader loader, object loadFromReaderParam)
		{
			SqlDataReader reader = null;
			short currentRetries = 0;
RetryExecute:
			try {
				using ( SqlConnection connection = new SqlConnection(_connectionString) ) {
					cmd.Connection = connection;
					cmd.Connection.Open();
					DataSet ds = new DataSet();
					SqlDataAdapter da = new SqlDataAdapter(cmd);
					da.Fill(ds);
					reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
					loader(reader, loadFromReaderParam);
				}
			} catch ( SqlException exception ) {
				if ( ( _deadlock != exception.Number ) || ( ( currentRetries = (short)( currentRetries + 1 ) ) >= _retries ) ) {
					throw;
				}
				goto RetryExecute;
			} finally {
				if ( ( reader != null ) && !reader.IsClosed ) {
					reader.Close();
				}
			}
		}

		private void loadActivityEventsFromReader(SqlDataReader reader, object parameter)
		{
			if ( reader == null ) {
				throw new ArgumentNullException("reader");
			}
			Dictionary<long, ActivityTrackingRecord> dictionary = new Dictionary<long, ActivityTrackingRecord>();
			DateTime time = SqlDateTime.MinValue.Value;
			while ( reader.Read() ) {
				string qualifiedName = reader.GetString(0);
				ActivityExecutionStatus executionStatus = (ActivityExecutionStatus)reader[1];
				DateTime eventDateTime = reader.GetDateTime(2);
				Guid contextGuid = reader.GetGuid(3);
				Guid parentContextGuid = reader.GetGuid(4);
				int eventOrder = reader.GetInt32(5);
				Type activityType = null;
				if ( !reader.IsDBNull(6) && !reader.IsDBNull(7) ) {
					activityType = Type.GetType(reader.GetString(6) + ", " + reader.GetString(7), false, false);
				}
				long statusEventId = reader.GetInt64(8);
				DateTime dbEventTime = reader.GetDateTime(9);
				ActivityTrackingRecord record = new ActivityTrackingRecord();
				record.QualifiedName = qualifiedName;
				record.ContextGuid = contextGuid;
				record.ParentContextGuid = parentContextGuid;
				record.ExecutionStatus = executionStatus;
				record.EventDateTime = eventDateTime;
				record.EventOrder = eventOrder;
				record.EventArgs = null;
				record.ActivityType = activityType;
				dictionary.Add(statusEventId, record);
				//dictionary.Add(key, new ActivityTrackingRecord(activityType, qualifiedName, contextGuid,
				//    guid, executionStatus, dateTime, eventOrder, null));
				if ( dbEventTime > time ) {
					time = dbEventTime;
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			while ( reader.Read() ) {
				long dataItemId = reader.GetInt64(0);
				string annotation = null;
				if ( !reader.IsDBNull(1) ) {
					annotation = reader.GetString(1);
				}
				ActivityTrackingRecord record = null;
				if ( dictionary.TryGetValue(dataItemId, out record) && ( record != null ) ) {
					record.Annotations.Add(annotation);
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			BinaryFormatter formatter = new BinaryFormatter();
			Dictionary<long, TrackingDataItem> dataItems = new Dictionary<long, TrackingDataItem>();
			while ( reader.Read() ) {
				long eventId = reader.GetInt64(0);
				long dataItemId = reader.GetInt64(1);
				string fieldName = reader.GetString(2);
				string sData = null;
				object oData = null;
				if ( !reader.IsDBNull(3) ) {
					sData = reader.GetString(3);
				}
				if ( !reader.IsDBNull(4) ) {
					try {
						oData = formatter.Deserialize(new MemoryStream((byte[])reader[4]));
					} catch { }
				}
				TrackingDataItem item = new TrackingDataItem
				{
					FieldName = fieldName
				};
				if ( oData != null ) {
					item.Data = oData;
				} else {
					item.Data = sData;
				}
				dataItems.Add(dataItemId, item);
				ActivityTrackingRecord activityRecord = null;
				if ( dictionary.TryGetValue(eventId, out activityRecord) && ( activityRecord != null ) ) {
					activityRecord.Body.Add(item);
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			while ( reader.Read() ) {
				long dataItemId = reader.GetInt64(0);
				string annotation = null;
				if ( !reader.IsDBNull(1) ) {
					annotation = reader.GetString(1);
				}
				TrackingDataItem dataItem = null;
				if ( dataItems.TryGetValue(dataItemId, out dataItem) && ( dataItem != null ) ) {
					dataItem.Annotations.Add(annotation);
				}
			}
			if ( _activityEvents == null ) _activityEvents = new List<ActivityTrackingRecord>();
			_activityEvents.AddRange(dictionary.Values);
			if ( time > SqlDateTime.MinValue.Value ) {
				_actMinDT = time;
			}
		}

		private void loadDef()
		{
			SqlCommand cmd = null;
			if ( _def == null ) {
				cmd = new SqlCommand
				{
					CommandType = CommandType.StoredProcedure,
					CommandText = "[dbo].[GetWorkflowDefinition]"
				};
				cmd.Parameters.Add(new SqlParameter("@WorkflowInstanceInternalId", _internalId));
				executeRetried(cmd, new LoadFromReader(loadDefFromReader));
			}
		}

		private void loadDefFromReader(SqlDataReader reader, object parameter)
		{
			if ( reader == null ) {
				throw new ArgumentNullException("reader");
			}
			if ( !reader.Read() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			StringReader input = new StringReader(reader.GetString(0));
			WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();
			DesignerSerializationManager serializationManager = new DesignerSerializationManager();
			IList errors = null;
			try {
				using ( serializationManager.CreateSession() ) {
					using ( XmlReader reader3 = XmlReader.Create(input) ) {
						_def = serializer.Deserialize(serializationManager, reader3) as Activity;
						errors = serializationManager.Errors;
					}
				}
			} finally {
				input.Close();
			}
			// ignore this for now.
			//if ( ( _def == null ) || ( ( errors != null ) && ( errors.Count > 0 ) ) ) {
			//    throw new WorkflowMarkupSerializationException("Deserialization Error.");
			//}
		}

		private void loadInvokedWorkflows()
		{
			SqlCommand cmd = new SqlCommand
			{
				CommandText = "[dbo].[GetInvokedWorkflows]",
				CommandType = CommandType.StoredProcedure
			};
			SqlParameter parameter = new SqlParameter("@WorkflowInstanceId", SqlDbType.UniqueIdentifier)
			{
				Value = _id
			};
			cmd.Parameters.Add(parameter);
			parameter = new SqlParameter("@BeginDateTime", SqlDbType.DateTime)
			{
				Value = _invMinDT
			};
			cmd.Parameters.Add(parameter);
			parameter = new SqlParameter("@EndDateTime", SqlDbType.DateTime)
			{
				Value = _currDT
			};
			cmd.Parameters.Add(parameter);
			executeRetried(cmd, new LoadFromReader(loadInvokedWorkflowsFromReader));
		}

		private void loadInvokedWorkflowsFromReader(SqlDataReader reader, object parameter)
		{
			if ( reader == null ) {
				throw new ArgumentNullException("reader");
			}
			DateTime initialized = SqlDateTime.MinValue.Value;
			while ( reader.Read() ) {
				TrackingWorkflowInstance item = TrackingQuery.BuildInstance(reader, _connectionString, null);
				if ( item.Initialized > initialized ) {
					initialized = item.Initialized;
				}
				_invoked.Add(item);
			}
			if ( initialized > SqlDateTime.MinValue.Value ) {
				_invMinDT = initialized;
			}
		}

		private void loadUserEvents()
		{
			SqlCommand cmd = createInternalIdDateTimeCommand("[dbo].[GetUserEventsWithDetails]", _userMinDT, _currDT);
			executeRetried(cmd, new LoadFromReader(loadUserEventsFromReader));
		}

		private void loadUserEventsFromReader(SqlDataReader reader, object parameter)
		{
			if ( reader == null ) {
				throw new ArgumentNullException("reader");
			}
			BinaryFormatter formatter = new BinaryFormatter();
			Dictionary<long, UserTrackingRecord> dictionary = new Dictionary<long, UserTrackingRecord>();
			DateTime time = SqlDateTime.MinValue.Value;
			while ( reader.Read() ) {
				string qualifiedName = reader.GetString(0);
				DateTime eventDate = reader.GetDateTime(1);
				Guid contextGuid = reader.GetGuid(2);
				Guid parentContextGuid = reader.GetGuid(3);
				int eventOrder = reader.GetInt32(4);
				string userDataKey = null;
				if ( !reader.IsDBNull(5) ) {
					userDataKey = reader.GetString(5);
				}
				object userData = null;
				if ( !reader.IsDBNull(7) ) {
					userData = formatter.Deserialize(new MemoryStream((byte[])reader[7]));
				} else if ( !reader.IsDBNull(6) ) {
					userData = reader.GetString(6);
				}
				if ( reader.IsDBNull(8) || reader.IsDBNull(9) ) {
					throw new InvalidOperationException(string.Format("Tracking Type '{0}' not found.", new object[] { qualifiedName }));
				}
				Type activityType = Type.GetType(reader.GetString(8) + ", " + reader.GetString(9), true, false);
				long key = reader.GetInt64(10);
				DateTime dbEventTime = reader.GetDateTime(11);
				dictionary.Add(key, new UserTrackingRecord(activityType, qualifiedName, contextGuid, parentContextGuid, eventDate, eventOrder, userDataKey, userData));
				if ( dbEventTime > time ) {
					time = dbEventTime;
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			while ( reader.Read() ) {
				long recordId = reader.GetInt64(0);
				string annotation = null;
				if ( !reader.IsDBNull(1) ) {
					annotation = reader.GetString(1);
				}
				UserTrackingRecord record = null;
				if ( dictionary.TryGetValue(recordId, out record) && ( record != null ) ) {
					record.Annotations.Add(annotation);
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			Dictionary<long, TrackingDataItem> dictionary2 = new Dictionary<long, TrackingDataItem>();
			while ( reader.Read() ) {
				long dataItemId = reader.GetInt64(0);
				long key = reader.GetInt64(1);
				string fieldName = reader.GetString(2);
				string sData = null;
				object oData = null;
				if ( !reader.IsDBNull(3) ) {
					sData = reader.GetString(3);
				}
				if ( !reader.IsDBNull(4) ) {
					oData = formatter.Deserialize(new MemoryStream((byte[])reader[4]));
				}
				TrackingDataItem item = new TrackingDataItem();
				item.FieldName = fieldName;
				if ( oData != null ) {
					item.Data = oData;
				} else {
					item.Data = sData;
				}
				dictionary2.Add(key, item);
				UserTrackingRecord record2 = null;
				if ( dictionary.TryGetValue(dataItemId, out record2) && ( record2 != null ) ) {
					record2.Body.Add(item);
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid ActivityEvent Reader");
			}
			while ( reader.Read() ) {
				long dataItemId = reader.GetInt64(0);
				string annotation = null;
				if ( !reader.IsDBNull(1) ) {
					annotation = reader.GetString(1);
				}
				TrackingDataItem dataItem = null;
				if ( dictionary2.TryGetValue(dataItemId, out dataItem) && ( dataItem != null ) ) {
					dataItem.Annotations.Add(annotation);
				}
			}
			_userEvents.AddRange(dictionary.Values);
			if ( time > SqlDateTime.MinValue.Value ) {
				_userMinDT = time;
			}
		}

		private void loadWorkflowEvents()
		{
			SqlCommand cmd = createInternalIdDateTimeCommand("[dbo].[GetWorkflowInstanceEventsWithDetails]", _instMinDT, _currDT);
			executeRetried(cmd, new LoadFromReader(loadWorkflowEventsFromReader));
		}

		private void loadWorkflowEventsFromReader(SqlDataReader reader, object parameter)
		{
			if ( reader == null ) {
				throw new ArgumentNullException("reader");
			}
			DateTime time = SqlDateTime.MinValue.Value;
			Dictionary<long, WorkflowTrackingRecord> dictionary = new Dictionary<long, WorkflowTrackingRecord>();
			while ( reader.Read() ) {
				TrackingWorkflowEvent trackingWorkflowEvent = (TrackingWorkflowEvent)reader[0];
				DateTime dateTime = reader.GetDateTime(1);
				int eventOrder = reader.GetInt32(2);
				object oEventArgs = null;
				EventArgs eventArgs = null;
				if ( !reader.IsDBNull(3) ) {
					oEventArgs = new BinaryFormatter().Deserialize(new MemoryStream((byte[])reader[3]));
					if ( oEventArgs is EventArgs ) {
						eventArgs = (EventArgs)oEventArgs;
					}
				}
				long key = reader.GetInt64(4);
				DateTime dbEventTime = reader.GetDateTime(5);
				dictionary.Add(key, new WorkflowTrackingRecord(trackingWorkflowEvent, dateTime, eventOrder, eventArgs));
				if ( dbEventTime > time ) {
					time = dbEventTime;
				}
			}
			if ( !reader.NextResult() ) {
				throw new ArgumentException("Invalid WorkflowInstance Reader");
			}
			while ( reader.Read() ) {
				long recordId = reader.GetInt64(0);
				string item = null;
				if ( !reader.IsDBNull(1) ) {
					item = reader.GetString(1);
				}
				WorkflowTrackingRecord record = null;
				if ( dictionary.TryGetValue(recordId, out record) && ( record != null ) ) {
					record.Annotations.Add(item);
				}
			}
			if ( !reader.IsClosed ) {
				reader.Close();
			}
			foreach ( KeyValuePair<long, WorkflowTrackingRecord> pair in dictionary ) {
				WorkflowTrackingRecord loadFromReaderParam = pair.Value;
				if ( TrackingWorkflowEvent.Changed == loadFromReaderParam.TrackingWorkflowEvent ) {
					SqlCommand cmd = new SqlCommand("[dbo].[GetWorkflowChangeEventArgs]");
					cmd.CommandType = CommandType.StoredProcedure;
					cmd.Parameters.Add(new SqlParameter("@WorkflowInstanceInternalId", _internalId));
					cmd.Parameters.Add(new SqlParameter("@BeginDateTime", SqlDateTime.MinValue.Value));
					cmd.Parameters.Add(new SqlParameter("@WorkflowInstanceEventId", pair.Key));
					//this.ExecuteRetried(cmd, new LoadFromReader(this.LoadWorkflowChangeEventArgsFromReader), loadFromReaderParam);
				}
			}
			_workflowEvents.AddRange(dictionary.Values);
			if ( time > SqlDateTime.MinValue.Value ) {
				_instMinDT = time;
			}
		}

		#endregion Methods

		#endregion Private Members

		#region Delegates

		private delegate void LoadFromReader(SqlDataReader reader, object parameter);

		#endregion Delegates
	}
}